/*
  internal storage  
 
*/


interface DataNode {
    public String getName();
    boolean isLeaf();
    public DataNodeIterator iterator(DataFilter filter);
}

public class DataFilter {
 
  private Date start; 
  private Date end;
  
  public DataFilter(DateRange range) {
    this.start = range.getStart();
    this.end = range.getEnd();
  } 

  // does date lie between start and end dates 
  public boolean dateMatch(Date date) {
    return (date.compareTo(start) >= 0) && (date.compareTo(end) <= 0);
  } 
}

public class DateRange {
  
  private Date start;
  private Date end;
  
  public DateRange(Date start, Date end) {
    if (start.compareTo(end) <= 0) {
      // start <= end
      this.start = start;
      this.end = end;
    } 
    else {
      // start > end ? swap them for sanity
      this.start = end;
      this.end = start;
    }
  }
  
  public Date getStart() {
    return start;  
  }
  
  public Date getEnd() {
    return end;  
  }
  
  public String toString() {
     DateFormat formatter = new SimpleDateFormat("yyyy-mm-dd hh:mm:ss"); 
     return formatter.format(start) + " " + formatter.format(end);
  }
}


public class DataNodeIterator implements Iterator {
  
   private int idx;
   private ArrayList items;
   private DataFilter filter;
   
   public DataNodeIterator(ArrayList items, DataFilter filter) {
     this.items = items;
     this.idx = 0;
     this.filter = filter;
   }
   
   public boolean hasNext() {
     return (items != null) && idx < items.size();  
   }
   
   public DataNode next() { 
     return (DataNode) items.get(idx++);
   }
   
   public void prev() { 
     if (idx > 0) {
       idx--;
     }
   }
   
   public void remove() {};
   
   public DataFilter getFilter() {
     return this.filter;
   }
}

public class PlayIterator extends DataNodeIterator {
     
  public PlayIterator(ArrayList items, DataFilter filter) {
    super(items, filter);
  }
   
  public boolean hasNext() {
    DataFilter filter = getFilter();
    while(super.hasNext()) {
      Play play = (Play) super.next();  
      if (filter == null || filter.dateMatch(play.getDate())) {
       super.prev();
       return true;  
      }
    }
    return false;
  }
} 



public void dumpDataNode(DataNode node) {
  println("["+node.getName()+"]");
  DataNodeIterator itor = node.iterator(null); 
  while (itor.hasNext()) {
    DataNode childNode = itor.next();  
    dumpDataNode(childNode);
  }
}

// represents an artist or a band
public class Band implements DataNode {

  private String name;
  private ArrayList albums;

  public Band(String name) {
    this.name = name;
    this.albums = new ArrayList();
  }
  

  public Album getAlbum(String name) {
    for (int i=0; i < albums.size(); i++) {
      Album album = (Album) albums.get(i);
      if (album.name.equalsIgnoreCase(name)) {
        return album;
      }
    }
    return null;
  }

  // add album if not already present
  Album addAlbum(String name) {
    Album album = getAlbum(name);
    if (album == null) {
      album = new Album(this, name);
      albums.add(album);
    }
    return album;      
  }
  
  public String toString(){
    return name;
  }
  
  public String getName() {
    return name;  
  }

  public boolean isLeaf() {
    return false;
  }
  
  public DataNodeIterator iterator(DataFilter filter) {
    return new DataNodeIterator(albums, filter);  
  }  
}

// represets an album produced by a band/artist
public class Album implements DataNode {

  private Band band; // parent
  private String name;
  private ArrayList tracks;

  public Album(Band band, String name) {
    this.band = band;
    this.name = name;
    this.tracks = new ArrayList();  
    // albumList.entries.add(name);
  }

  Track getTrack(String name) {
    for (int i=0; i < tracks.size(); i++) {
      Track track = (Track) tracks.get(i);
      if (track.name.equalsIgnoreCase(name)) {
        return track;
      }
    }
    return null;  
  }
  
  String toString(){
    return name;
  }

  // add track if not already present
  Track addTrack(String name) {
    Track track = getTrack(name);
    if (track == null) {
      track = new Track(this, name); 
      tracks.add(track);
    }
    return track;
  }
  
  public String getName() {
    return name;  
  }

  public boolean isLeaf() {
    return false;
  }
  
  public DataNodeIterator iterator(DataFilter filter) {
    return new DataNodeIterator(tracks, filter);  
  } 
}

// used by track class to group a user plays together
public class UserPlayGroup implements DataNode {
 
  private User user;
  ArrayList plays;
  
  public UserPlayGroup(User user) {
    this.user = user;
    this.plays = new ArrayList();    
  }
  
  public void addPlay(Play play) {
    plays.add(play);
  }

  public String getName() {
    return user.getName();
  }

  public boolean isLeaf() {
    return false;
  }
  
  public DataNodeIterator iterator(DataFilter filter) {
    return new PlayIterator(plays,filter);  
  }  
}


// represents track produced by artist/band on a particular album
public class Track implements DataNode {

  private Album album; // parent
  private String name;
  private ArrayList groups; 

  public Track(Album album, String name) {
    this.album = album;
    this.name = name;
    this.groups = new ArrayList();
  }
  
  private UserPlayGroup findGroup(String name) {
    for (int i=0; i < groups.size(); i++) {
      UserPlayGroup group = (UserPlayGroup) groups.get(i);
      if (group.getName().equals(name)) {
        return group;  
      }
    }
    return null;
  }
  
  private UserPlayGroup getGroup(User user) {
    UserPlayGroup group = findGroup(user.getName());
    if (group == null) {
      group = new UserPlayGroup(user);
      groups.add(group);
    }
    return group;
  }

  public void addPlay(Play play) {
    UserPlayGroup group = getGroup(play.user);
    group.addPlay(play); 
  }
  
  public String toString(){
    return name;
  }
  
  public String getName() {
    return name;  
  }

  public boolean isLeaf() {
    return false;
  }
  
  public DataNodeIterator iterator(DataFilter filter) {
    return new DataNodeIterator(groups,filter);  
  }
}

// used by user class to group a track plays together
public class TrackPlayGroup implements DataNode {
 
  private Track track;
  ArrayList plays;
  
  public TrackPlayGroup(Track track) {
    this.track = track;
    this.plays = new ArrayList();    
  }
  
  public void addPlay(Play play) {
    plays.add(play);
  }

  public String getName() {
    return track.getName();
  }
  
  public boolean isLeaf() {
    return false;
  }
  
  public DataNodeIterator iterator(DataFilter filter) {
    return new PlayIterator(plays, filter);  
  }
}

public class User implements DataNode {

  private String name;
  private ArrayList groups;

  public User(String name) {
    this.name = name;
    this.groups = new ArrayList();
  }
  
  private TrackPlayGroup findGroup(String name) {
    for (int i=0; i < groups.size(); i++) {
      TrackPlayGroup group = (TrackPlayGroup) groups.get(i);
      if (group.getName().equals(name)) {
        return group;  
      }
    }
    return null;
  }
  
  private TrackPlayGroup getGroup(Track track) {
    TrackPlayGroup group = findGroup(track.getName());
    if (group == null) {
      group = new TrackPlayGroup(track);
      groups.add(group);
    }
    return group;
  }

  public void addPlay(Play play) {
    TrackPlayGroup group = getGroup(play.track);
    group.addPlay(play);
  }

  public String toString(){
    return name;
  }
  
  public String getName() {
    return name;  
  }
  
  public boolean isLeaf() {
    return false;
  }
  
  public DataNodeIterator iterator(DataFilter filter) {
    return new DataNodeIterator(groups, filter);  
  }
  
}

// date/time a user played a particular track
public class Play implements DataNode {

  private User user;
  private Track track;
  private Date date;

  public Play(User user, Track track, Date date) {
    this.user = user;
    this.track = track;
    this.date = date;
  }

  public User getUser() {
    return user;  
  }
  
  public Track getTrack() {
    return track;
  }
  
  public Date getDate() {
    return date;
  }
 
  public String getName() {
    return date.toString();  
  }
  
  public boolean isLeaf() {
    return true;
  }
  
  public DataNodeIterator iterator(DataFilter filter) {
    return new DataNodeIterator(null,null);  
  }  
}


abstract class DataGroup implements DataNode {
  
  private String name;
  private ArrayList nodeList;
  
  public DataGroup(String name) {
    this.name = name;
    this.nodeList = new ArrayList();
  }
  
  public void addList(DataNode node) {
    nodeList.add(node);  
  }
    
  public int size() {
    return nodeList.size();  
  }
  
  public ArrayList values() {
    return this.nodeList;  
  }

  public String getName() {
    return name;  
  }
 
  public boolean isLeaf() {
    return false;
  }
   
  public DataNodeIterator iterator(DataFilter filter) {
    return new DataNodeIterator(nodeList, filter);  
  }

  public void sortList() {
    Collections.sort(nodeList, new Comparator() {
      public int compare(Object o1, Object o2) {
          DataNode n1 = (DataNode) o1;
          DataNode n2 = (DataNode) o2;
          return n1.getName().compareTo(n2.getName());
      }
    });
  }  
}

abstract class DataGroupHash extends DataGroup {
  
  private HashMap nodeMap;
  
  public DataGroupHash(String name) {
    super(name);
    this.nodeMap = new HashMap();
  }
  
  private String makeKey(String s) {
    return s.trim().toUpperCase();
  }
  
  // create a new subtype
  abstract DataNode createNode(String name);
  
  public DataNode addNode(String name) {
    String key = makeKey(name);
    DataNode node = (DataNode) nodeMap.get(key);
    if (node == null) {
      node = createNode(name);
      nodeMap.put(key, node);
      addList(node);
    }
    return node;
  }
  
  public DataNode getNode(String name) {
    return (DataNode) nodeMap.get(makeKey(name));
  }
  
   
}

public class BandGroup extends DataGroupHash {
  
  public BandGroup(String name) {
    super(name);
  }

  public Band createNode(String name) {
    return new Band(name);
  }
  
  public Band addBand(String name) {
    return (Band) super.addNode(name);  
  }
}

public class UserGroup extends DataGroupHash {
  
  public UserGroup(String name) {
    super(name);
  }

  public User createNode(String name) {
    return new User(name);
  }
  
  public User addUser(String name) {
    return (User) super.addNode(name);  
  }
}

public class TrackGroup extends DataGroup {

  public TrackGroup(String name) {
    super(name);
  }  
}



public class DataBase {
  
  private BandGroup bands;
  private UserGroup users;
  private TrackGroup tracks;

  private ArrayList albums;

  // MUST be sorted by date/time
  private ArrayList plays;
  
  // track plays date range
  private Date minDate;
  private Date maxDate;

  public DataBase(String fname) {
    this.bands = new BandGroup("All Bands");
    this.users = new UserGroup("All Users");
    this.tracks = new TrackGroup("All Tracks");
 
    this.albums = new ArrayList();
    this.plays = new ArrayList();
    this.minDate = null;
    this.maxDate = null;
    this.addFile(fname);  
  }

  public DataGroup getUsers(){
    return this.users;
  }
  
  public DataGroup getBands(){
    return this.bands;
  }
  
  public DataGroup getTracks(){
    return this.tracks;
  }
  
  public ArrayList getPlays(){
    return this.plays;
  }
  
  public ArrayList getAlbums(){
    return this.albums;
  }

  private Album addAlbum(Band band, String name) {
    boolean addit = band.getAlbum(name) == null ? true : false;
    Album album = band.addAlbum(name);
    if (addit) {
      albums.add(album);  
    }
    return album;   
  } 

  private Track addTrack(Album album, String name) {
    boolean addit = album.getTrack(name) == null ? true : false;
    Track track = album.addTrack(name);
    if (addit) {
      tracks.addList(track);
    }
    return track;
  }
  
  private void updateDateRange(Date date) { 
    if (minDate == null || date.compareTo(minDate) < 0) {
      minDate = date;  
    }
    if (maxDate == null || date.compareTo(maxDate) > 0) {
      maxDate = date;
    }
  }
  
  public Date getMinDate() {
    return minDate;
  }
  
  public Date getMaxDate() {
     return maxDate; 
  }

  public Band getBand(String name) {
    return (Band) bands.getNode(name);
  }
  
  public User getUser(String name) { 
    return (User) users.getNode(name);
  }
  
  public void addRecord(DataFileRecord record) {
    // locate entities
    User user = users.addUser(record.user);
    Band band = bands.addBand(record.band);
    Album album = addAlbum(band, record.album);
    Track track = addTrack(album, record.track);
    // create new entry and links to parents
    Play play = new Play(user, track, record.dateTime);    
    plays.add(play);
    user.addPlay(play);
    track.addPlay(play);
    updateDateRange(play.getDate());
  }

  public void addFile(String fname) {
    DataFileReader reader = new DataFileReader(fname);
    DataFileRecord record;
    while ( (record = reader.readRecord()) != null) {
      addRecord(record);  
    }
    reader.close();
  }
  
  public void sort() {
    users.sortList();  
    bands.sortList();
    tracks.sortList();
  }
  
  public DateRange getDateRange() {
    return new DateRange(minDate,maxDate);
  }

  String toString() {
    return String.format("%d Users, %d Bands, %d Tracks, %d Plays", 
    users.size(), bands.size(), tracks.size(), plays.size());  
  }

}

